/*
 * Unidata Platform Community Edition
 * Copyright (c) 2013-2020, UNIDATA LLC, All rights reserved.
 * This file is part of the Unidata Platform Community Edition software.
 *
 * Unidata Platform Community Edition is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Unidata Platform Community Edition is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 */
package org.unidata.mdm.meta.service.impl.data.refresh;

import org.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.unidata.mdm.core.context.ModelRefreshContext;
import org.unidata.mdm.core.dto.SecuredResourceDTO;
import org.unidata.mdm.core.service.RoleService;
import org.unidata.mdm.core.type.model.AttributeElement;
import org.unidata.mdm.core.type.model.EntityElement;
import org.unidata.mdm.core.type.security.SecuredResourceCategory;
import org.unidata.mdm.core.type.security.SecuredResourceType;
import org.unidata.mdm.core.util.SecurityUtils;
import org.unidata.mdm.meta.configuration.Descriptors;
import org.unidata.mdm.meta.service.impl.data.instance.LookupImpl;
import org.unidata.mdm.meta.service.impl.data.instance.RegisterImpl;
import org.unidata.mdm.meta.type.instance.DataModelInstance;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * @author Mikhail Mikhailov on Nov 2, 2020
 */
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class SecurityResourcesRefreshListener extends AbstractDataModelRefreshListener {
    /**
     * Role service. Contains methods for role management.
     */
    @Autowired
    private RoleService roleService;
    /**
     * Constructor.
     */
    public SecurityResourcesRefreshListener() {
        super();
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public void refresh(ModelRefreshContext refresh) {

        String storageId = SecurityUtils.getStorageId(refresh);
        DataModelInstance fresh = metaModelService.instance(Descriptors.DATA, storageId, null);
        DataModelInstance p = getPreviousState(refresh);

        int revision = fresh.getVersion();
        boolean isNew = revision == 1;

        List<EntityElement> updated;
        if (!isNew) {

            List<EntityElement> deleted
                = Stream.concat(p.getRegisters().stream().filter(r -> Objects.isNull(fresh.getRegister(r.getName()))),
                                p.getLookups().stream().filter(l -> Objects.isNull(fresh.getLookup(l.getName()))))
                    .collect(Collectors.toList());

            // 1. Should delete resources recursive
            deleteResources(deleted.stream()
                    .map(EntityElement::getName)
                    .collect(Collectors.toList()));
        }

        updated
            = Stream.concat(fresh.getRegisters().stream().filter(r -> ((RegisterImpl) r).getSource().getVersion() == fresh.getVersion()),
                            fresh.getLookups().stream().filter(l -> ((LookupImpl) l).getSource().getVersion() == fresh.getVersion()))
                .collect(Collectors.toList());

        // 2. Update
        updated.forEach(e -> {

            if (!isNew && p.isElement(e.getName())) {

                EntityElement old = p.getElement(e.getName());
                List<String> removed = old.getAttributes().entrySet().stream()
                    .filter(attr -> !e.getAttributes().containsKey(attr.getKey()))
                    .map(Entry::getValue)
                    .map(attr -> String.join(".", e.getName(), attr.getPath()))
                    .collect(Collectors.toList());

                deleteResources(removed);
            }

            createResources(e.getName(), e.getDisplayName(), e.getAttributes());
        });
    }

    /* (non-Javadoc)
     * @see com.unidata.mdm.backend.service.security.ISecurityService#deleteResources(java.util.List)
     */
    private void deleteResources(List<String> resources) {

        if (CollectionUtils.isEmpty(resources)) {
            return;
        }

        for (String resource : resources) {
            roleService.deleteResource(resource);
        }
    }

    private SecuredResourceDTO createResource(AttributeElement attr, String topLevelName, SecuredResourceDTO parent) {

        SecuredResourceDTO resource = new SecuredResourceDTO();
        resource.setName(String.join(".", topLevelName, attr.getPath()));
        resource.setDisplayName(attr.getDisplayName());
        resource.setParent(parent);
        resource.setCreatedAt(new Date());
        resource.setCreatedBy(SecurityUtils.getCurrentUserName());
        resource.setType(SecuredResourceType.USER_DEFINED);
        resource.setCategory(parent.getCategory());
        resource.setUpdatedAt(new Date());
        resource.setUpdatedBy(SecurityUtils.getCurrentUserName());

        if (attr.hasChildren()) {
            List<SecuredResourceDTO> children = new ArrayList<>();
            for (AttributeElement child : attr.getChildren()) {
                children.add(createResource(child, topLevelName, resource));
            }

            resource.setChildren(children);
        }

        return resource;
    }

    private void createResources(String name, String displayName, Map<String, AttributeElement> attrs) {

        // 1. Top level object
        SecuredResourceDTO resource = new SecuredResourceDTO();
        resource.setName(name);
        resource.setDisplayName(displayName);
        resource.setCreatedAt(new Date());
        resource.setCreatedBy(SecurityUtils.getCurrentUserName());
        resource.setType(SecuredResourceType.USER_DEFINED);
        resource.setCategory(SecuredResourceCategory.META_MODEL.name());
        resource.setUpdatedAt(new Date());
        resource.setUpdatedBy(SecurityUtils.getCurrentUserName());

        // 2. Attributes. Only top level are processed
        List<SecuredResourceDTO> children = new ArrayList<>();
        for (Entry<String, AttributeElement> entry : attrs.entrySet()) {

            if (entry.getValue().hasParent()) {
                continue;
            }

            children.add(createResource(entry.getValue(), name, resource));
        }

        resource.setChildren(children);

        // 3. Execute
        roleService.createResources(Collections.singletonList(resource));
    }
}
